裝飾器可以使我們可以在類別、方法、屬性或參數上添加元數據(metadata),並根據這些元數據來自動轉化或擴充程式碼。它可以在類別或方法不修改程式碼的情況下做一些特定的處理,同時也把可以重複使用的邏輯拆分出去。
因為裝飾器在 JavaScript 裡仍然為實驗性語法,所以在 TypeScript 裡使用裝飾器之前,要先在 tsconfig.json 加入 experimentalDecorators 為 true
,這樣裝飾器才可以正常運作,不然會報錯。
// tsconfig.json
{
"compilerOptions": {
// ...
"experimentalDecorators": true,
// "emitDecoratorMetadata": true,
// ...
}
}
注意:如果需要想要使用一些實驗性的 metadata API ( ex: reflect-metadata ),因為它還未成為 JavaScript 標準的一部分,所以需要再開啟
emitDecoratorMetadata 為 true
。
裝飾器使用方式:@函式名稱
。
這個函式可以接收一個或多個參數,具體取決於是什麼類型的裝飾器。一般會有以下幾種裝飾器:
類別裝飾器用於修改或擴充類別的定義,它接收一個 target 參數
,代表被裝飾類別的建構函式。
類別裝飾器在類別聲明之前聲明。
看以下範例:
const classDecorator: ClassDecorator = (target: any): void => {
target.prototype.greet = () => {
console.log("哈囉,威爾豬!");
};
};
@classDecorator
class MyClass {}
const myClass = new MyClass() as any;
myClass.greet(); // 輸出: 哈囉,威爾豬!
在這個範例中,classDecorator 是一個簡單的類別裝飾器,我們在類別的建構函式原型鏈上加上 greet 方法,它被應用在 MyClass 類別上。所以 myClass 實體可以使用 greet 方法。
方法裝飾器用於修改或擴充類別的方法,它 接收三個參數,分別是 target、propertyKey 和 descriptor
。target 代表被裝飾類別的建構函式,propertyKey 代表被裝飾的方法名稱,descriptor 則是儲存屬性的選項。
方法裝飾器在方法聲明之前聲明。
看以下範例:
const methodDecorator: MethodDecorator = (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
): void => {
const originalMethod = descriptor.value;
descriptor.value = function (...args: number[]) {
const result = originalMethod.apply(this, args);
console.log(result);
};
};
class Calculator {
@methodDecorator
increase(a: number, b: number): number {
return a + b;
}
@methodDecorator
decrease(a: number, b: number): number {
return a - b;
}
}
const calculator = new Calculator();
calculator.increase(2, 3); // 輸出: 5
calculator.decrease(2, 3); // 輸出: -1
在這個範例中,methodDecorator 方法裝飾器被應用在 Calculator 類別的 increase 和 decrease 方法上。當呼叫 increase 和 decrease 方法時,方法裝飾器中的程式碼將在方法執行前被呼叫。
這邊要注意的是,若要在 descriptor 參數的 value 欄位中呼叫原本的方法,最好用 call 或 apply 函式,並代入 this 作為參數來呼叫,這樣才可以確保 this 是該物件本身。
屬性裝飾器用於修改或擴充類別的屬性,它 接收兩個參數,分別是 target 和 propertyKey
。target 代表被裝飾類別的建構函式,propertyKey 代表被裝飾的屬性名稱。
屬性裝飾器在屬性聲明之前聲明。
看以下範例:
const propertyDecorator: PropertyDecorator = (
target: any,
propertyKey: string
): void => {
let value: string = target[propertyKey];
const getter = () => value;
const setter = (newValue: string) => {
value = `哈囉,${newValue}!`;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
});
};
class Person {
@propertyDecorator
readonly name: string = "威爾豬";
}
const person = new Person();
console.log(person.name); // 輸出: 哈囉,威爾豬!
在這個範例中,propertyDecorator 屬性裝飾器被應用在 Person 類別的 name 屬性上。當我們使用 name 屬性時,屬性裝飾器會將屬性值重組字串。
參數裝飾器用於修改或擴充方法參數,它 接收三個參數,分別是 target、propertyKey 和 parameterIndex
。target 代表被裝飾類別的建構函示,propertyKey 代表被裝飾的參數所在的方法名稱,parameterIndex 代表參數在函式中的 index 位置 (由 0 開始)。
參數裝飾器在參數聲明之前聲明。
看以下範例:
const parameterDecorator: ParameterDecorator = (
target: any,
propertyKey: string,
parameterIndex: number
): void => {
console.log(
`參數位置 ${parameterIndex} 被類別 ${target.constructor.name} 的 ${propertyKey} 方法使用`
);
};
class Calculator {
add(@parameterDecorator a: number, @parameterDecorator b: number): number {
return a + b;
}
}
const calculator = new Calculator();
console.log(calculator.add(2, 3));
// 輸出
// 參數位置 1 被類別 Calculator 的 add 方法使用
// 參數位置 0 被類別 Calculator 的 add 方法使用
// 5
在這個範例中,parameterDecorator 參數裝飾器被應用在 Calculator 類別的 add 方法的兩個參數上。當呼叫 calculator.add(2, 3) 時,參數裝飾器中的程式碼將被呼叫,用於記錄參數的使用。
那如果我們想要在裝飾器傳入參數怎麼辦?這時我們就要使用到 裝飾器工廠
。
裝飾器工廠其實就是在裝飾器外再包一層函式,外層的函式要回傳的則是真正的裝飾器,這時外層接收的參數就可以在內層的裝飾器中使用,這其實也是閉包
的一種應用。
看以下範例:
const classDecorator = (name: string, message: string) => {
const realDecorator: ClassDecorator = (target: any): void => {
target.prototype.greet = () => {
console.log(`哈囉,${name}${message}!`);
};
};
return realDecorator;
};
@classDecorator("威爾豬", "你好")
class MyClass {}
const myClass = new MyClass() as any;
myClass.greet(); // 輸出: 哈囉,威爾豬你好!
在這個範例中,classDecorator 傳入一個 name 參數,這時我們就可以在真正的類別裝飾器 realDecorator 上進行 name 參數的使用。
裝飾器的執行是 由內往外
,而在同一個類別、方法或屬性上使用的裝飾器是 由下往上
,而方法內使用參數裝飾器是 從後往前
,而且會 先執行參數裝飾器再執行方法裝飾器
。
看以下範例:
const classDecorator1: ClassDecorator = (): void => {
console.log("我是類別裝飾器 1");
};
const classDecorator2: ClassDecorator = (): void => {
console.log("我是類別裝飾器 2");
};
const methodDecorator1: MethodDecorator = (): void => {
console.log("我是方法裝飾器 1");
};
const methodDecorator2: MethodDecorator = (): void => {
console.log("我是方法裝飾器 2");
};
const propertyDecorator1: PropertyDecorator = (): void => {
console.log("我是屬性裝飾器 1");
};
const propertyDecorator2: PropertyDecorator = (): void => {
console.log("我是屬性裝飾器 2");
};
const parameterDecorator1: ParameterDecorator = (): void => {
console.log("我是參數裝飾器 1");
};
const parameterDecorator2: ParameterDecorator = (): void => {
console.log("我是參數裝飾器 2");
};
@classDecorator1 // 執行順序 8
@classDecorator2 // 執行順序 7
class MyClass1 {
@propertyDecorator1 // 執行順序 2
@propertyDecorator2 // 執行順序 1
name: string;
constructor() {
console.log("我是建構函式 1"); // 執行順序 13
}
@methodDecorator1 // 執行順序 6
@methodDecorator2 // 執行順序 5
add(
@parameterDecorator1 // 執行順序 4
a: number,
@parameterDecorator2 // 執行順序 3
b: number
): void {
console.log(a + b); // 執行順序 15
}
}
@classDecorator1 // 執行順序 12
@classDecorator2 // 執行順序 11
class MyClass2 {
@propertyDecorator1 // 執行順序 10
@propertyDecorator2 // 執行順序 9
age: string;
constructor() {
console.log("我是建構函式 2"); // 執行順序 14
}
multiple(a: number, b: number): void {
console.log(a * b); // 執行順序 16
}
}
const myClass1 = new MyClass1();
const myClass2 = new MyClass2();
myClass1.add(2, 3); // 輸出: 5
myClass2.multiple(4, 5); // 輸出: 20
// 輸出:
// 我是屬性裝飾器 2
// 我是屬性裝飾器 1
// 我是參數裝飾器 2
// 我是參數裝飾器 1
// 我是方法裝飾器 2
// 我是方法裝飾器 1
// 我是類別裝飾器 2
// 我是類別裝飾器 1
// 我是屬性裝飾器 2
// 我是屬性裝飾器 1
// 我是類別裝飾器 2
// 我是類別裝飾器 1
// 我是建構函式 1
// 我是建構函式 2
// 5
// 20
每種類型的裝飾器都有其特定的應用場景,並可以用於實現不同的功能。它們可以應用在類別、方法、屬性和參數上,並在開發框架、函式庫等有廣泛的應用,例如路由、驗證、log 記錄等。在使用裝飾器時應考慮程式碼的結構和可讀性,並謹慎選擇適當的裝飾器來實現需求,避免濫用,過多的裝飾器可能會使程式碼變得複雜和難以維護。